package de.sendorian.app.forumArchive;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Sets;

import de.sendorian.app.forumArchive.domain.Forum;
import de.sendorian.app.forumArchive.domain.Post;
import de.sendorian.app.forumArchive.domain.Topic;
import de.sendorian.app.forumArchive.finder.Finder;
import de.sendorian.app.forumArchive.repository.ForumRepository;
import de.sendorian.app.forumArchive.repository.TopicRepository;


/**
 * This class updates a topic, if necessary.
 * 
 * @author sendorian
 * @since 1.0 beta3
 * 
 */
@Service
public class TopicUpdater {

    private static final Logger LOG = Logger.getLogger(TopicUpdater.class);

    private TopicRepository topicRepository;
    private ForumRepository forumRepository;
    private Finder<Forum, Topic> topicFinder;


    /**
     * Checks if the topics are outdated or not yet persistent and updates/saves
     * them if necessary. Persists the changes in the given forum as well.
     * 
     * @param forum
     *            A persistent forum to search for changes in topics (will be
     *            changed).
     * @return A new list containing all supplied topics now updated and
     *         containing all posts.
     */
    public Set<Topic> updateTopicsInForum(@Nonnull Forum forum) {
        return updateTopics(topicFinder.find(forum), forum);
    }


    /**
     * Checks if the topics are outdated or not yet persistent and updates/saves
     * them if necessary.
     * 
     * @param topics
     *            A list of non-persistent topics.
     * @param forum
     *            A persistent forum where this topic should be contained, if
     *            persisted before (will be changed).
     * @return A new Set containing all supplied topics now updated and
     *         containing all posts.
     */
    public Set<Topic> updateTopics(@Nonnull List<Topic> topics, @Nonnull Forum forum) {
        Set<Topic> resultList = new HashSet<Topic>(topics.size());
        for (Topic topic : topics) {
            resultList.add(updateTopic(topic, forum));
        }
        return resultList;
    }


    /**
     * Checks if the topic is outdated or not yet persistent and updates/saves
     * it if necessary.
     * 
     * @param topic
     *            A non-persistent topic (will be changed).
     * @param forum
     *            A persistent forum where this topic should be contained, if
     *            persisted before (will be changed).
     * @return The supplied topic, now updated and containing all posts.
     */
    public Topic updateTopic(@Nonnull Topic topic, @Nonnull Forum forum) {
        if (!forum.getTopics().contains(topic)) {
            addTopicToForum(topic, forum);
        } else {
            doUpdateTopic(topic, forum);
        }
        return topic;
    }


    /**
     * Adds the given topic to the forum and saves the changed forum to the
     * database.
     */
    private void addTopicToForum(@Nonnull Topic topic, @Nonnull Forum forum) {
        LOG.info("Topic \"" + topic.getName() + "\" is new. Adding...");
        forum.getTopics().add(topic);
        forumRepository.saveOrUpdate(forum);
    }


    /**
     * Returns true if the topic is not yet persistent or outdated.
     * 
     * @param topic
     *            A non-persistent topic.
     * @param forum
     *            A persistent forum where this topic should be contained, if
     *            persisted before.
     * 
     */
    public boolean hasToBeUpdated(@Nullable final Topic topic, @Nonnull final Forum forum) {
        if (topic.getPosts().isEmpty()) {
            LOG.debug("For topic \"" + topic.getName() + "\" there were no posts in the given date range. Nothing to update.");
            return false;
        }
        
        Predicate<Topic> uptodatePredicate = new Predicate<Topic>() {
            public boolean apply(Topic persistentTopic) {
                if (!persistentTopic.equals(topic))
                    return false;

                // If no last last post is given the topic has to be checked!
                if (persistentTopic.getLastPost() == null || topic.getLastPost() == null) {
                    return true;
                }

                DateTime persistentLastChange = new DateTime(persistentTopic.getLastPost());
                DateTime onlineLastChange = new DateTime(topic.getLastPost());
                if (onlineLastChange.isAfter(persistentLastChange)) {
                    LOG.debug("Topic \"" + topic.getName() + "\" is outdated (DB: " + persistentLastChange
                            + ", Online: " + onlineLastChange + ").");
                    return false;
                }
                LOG.info("Topic \"" + topic.getName() + "\" is uptodate.");
                return true;
            }
        };
        return !Iterators.any(forum.getTopics().iterator(), uptodatePredicate);
    }


    /**
     * Updates the outdated Topic in the forum or adds it to the forums' topics
     * if it was not persisted before.
     * 
     * @param topic
     *            A non-persistent topic (will be changed).
     * @param forum
     *            A persistent forum where this topic should be contained, if
     *            persisted before (will be changed).
     */
    private void doUpdateTopic(@Nonnull Topic topic, @Nonnull Forum forum) {
        if (!hasToBeUpdated(topic, forum)) {
            return;
        }

        LOG.info("Updating posts for topic \"" + topic.getName() + "\"");
        Topic persistentTopic = getPersistentTopic(topic, forum);

        Set<Post> difference = Sets.difference(new HashSet<Post>(topic.getPosts()), new HashSet<Post>(persistentTopic
                .getPosts()));

        if (difference.size() == 0) {
            throw new IllegalStateException("For topic " + topic.getName() + " hasToBeUpdated returned true, "
                    + "therefore the last post of the two threads have "
                    + "different dates, but the difference of the posts was empty. That is not correct!");
        }

        persistentTopic.getPosts().addAll(difference);
        topicRepository.saveOrUpdate(persistentTopic);
        topic.setPosts(new ArrayList<Post>(persistentTopic.getPosts()));
    }


    /**
     * Returns the persistent topic in the forum that is equal to the given
     * topic.
     * 
     * @param topic
     *            A non-persistent topic.
     * @param forum
     *            A persistent forum where this topic should be contained, if
     *            persisted before.
     */
    private Topic getPersistentTopic(@Nonnull final Topic topic, @Nonnull Forum forum) {
        return Iterables.find(forum.getTopics(), Predicates.equalTo(topic));
    }


    @Resource(name = "topicRepository")
    public void setTopicRepository(TopicRepository topicRepository) {
        this.topicRepository = topicRepository;
    }


    @Resource(name = "forumRepository")
    public void setForumRepository(ForumRepository forumRepository) {
        this.forumRepository = forumRepository;
    }


    @Value("#{config.topicFinder}")
    public void setTopicFinder(Finder<Forum, Topic> topicFinder) {
        this.topicFinder = topicFinder;
    }

}
